package clasificador;

import java.util.HashMap;
import java.util.Map;
import java.util.List;
import java.util.ArrayList;

import datos.Atributo;
import datos.Dato;
import datos.MuestraClasificada;
import javax.management.ReflectionException;

public class ValidadorCruzado<Clase> 
    implements Validador<Clase,MuestraClasificada<Clase>,ValidadorCruzado.ResultadosValidacionCruzada> {

	private Class<? extends ClasificadorEntrenado<Clase>> clasificadores;
	private int nDivisionesMuestra, nDivisionesTest;
	
	public ValidadorCruzado(Class<? extends ClasificadorEntrenado<Clase>> clasificadores, 
            int nDivisionesMuestra, int nDivisionesTest)
	{
		this.clasificadores = clasificadores;
		this.nDivisionesMuestra = nDivisionesMuestra;
		this.nDivisionesTest = nDivisionesTest;
	}

	public ResultadosValidacionCruzada validar(MuestraClasificada<Clase> muestra, Map<String,Object> parametrosClasificador)
	{
		List<MuestraClasificada<Clase>.RangoClasificado> rangos = getDatosTestValidacionCruzada(muestra);
                List<Double> errores = new ArrayList<Double>();
		
		for(MuestraClasificada<Clase>.RangoClasificado rango: rangos)
		{
			ClasificadorEntrenado<Clase> clasificador;
			Atributo<Clase> clase = muestra.getClase();
            MuestraClasificada<Clase> datosEntrenamiento = rango.getOutside();
            MuestraClasificada<Clase> datosTest = rango.getInside();
			
			/* Es aqui donde se produce el entrenamiento */
            /* Aunque no lo parezca */
			try
			{
                for(Object o: clasificadores.getConstructors())
                {
                    System.out.println(o);
                }
                                
                clasificador = clasificadores.getConstructor(new Class<?>[] { MuestraClasificada.class, Map.class })
                    .newInstance(datosEntrenamiento, parametrosClasificador);

            } catch (Throwable e) {
                throw new RuntimeException("Reflection exception: " + e.getMessage(), e);
			}

            System.out.println("CLASIFICANDO");
			Map<Dato,Clase> resultadosTest = clasificarDatosTest(clasificador, datosTest);
            System.out.println("FIN CLASIFICACION");
			errores.add(calcularError(resultadosTest, clase));
		}
		
		return new ResultadosValidacionCruzada(errores);
	}	
	
	private Double calcularError(Map<Dato,Clase> resultadosTest, Atributo<Clase> clase)
	{
		int error = 0;
		int total = 0;
		
		for(Map.Entry<Dato,Clase> entry: resultadosTest.entrySet())
		{
			if(!entry.getKey().getAtributo(clase).equals(entry.getValue()))
			{
				error++;
			}
			total++;			
		}
		
		return new Double(error)/new Double(total);
	}
	
	private Map<Dato,Clase> clasificarDatosTest(ClasificadorEntrenado<Clase> clasificador, 
            MuestraClasificada<Clase> muestraTest)
	{
		Map<Dato,Clase> resultados = new HashMap<Dato,Clase>(muestraTest.size());
		
		for(Dato datoTest: muestraTest.getDatos())
		{
			resultados.put(datoTest, clasificador.clasificar(datoTest));
		}
		
		return resultados;
	}
	
	private MuestraClasificada<Clase>.RangoClasificado getDatosTest(MuestraClasificada<Clase> muestra, int iTest)
	{
		int nDatos = muestra.getDatos().size();
		
		double sizeDivision = new Double(nDatos) / new Double(nDivisionesMuestra);
		
		int jTest = new Double(sizeDivision*iTest).intValue();
		
		/* Con toda la cara, si se sale del conjunto, trunco el conjunto de test */
		/* Lo suyo ser�a dar la vuelta y hacer un rango, por ejemplo, de 90 a 100 y de 0 a 10; pero nuestra definici�n de
		 * RangoMuestra no permite eso, y he preferido no tocarla.
		 * Como en la mayor�a de los casos, nDivisionesTest va a ser 1, da igual.
		 */
		int jFinTest = Math.min(nDatos, jTest + new Double(sizeDivision*nDivisionesTest).intValue());
		
		return muestra.new RangoClasificado(jTest,jFinTest);
	}
	
	/* Devuelve varias particiones de validaci�n, para hacer validaci�n cruzada */
	private List<MuestraClasificada<Clase>.RangoClasificado> 
        getDatosTestValidacionCruzada(MuestraClasificada<Clase> muestra)
	{
		List<MuestraClasificada<Clase>.RangoClasificado> result = 
            new ArrayList<MuestraClasificada<Clase>.RangoClasificado>(nDivisionesMuestra);
		
		for(int i = 0; i < nDivisionesMuestra; i++)
		{
			result.add(getDatosTest(muestra,i));
		}
		
		return result;
	}
	
	public static class ResultadosValidacionCruzada
	{
		private List<Double> errores;
		
		public ResultadosValidacionCruzada(List<Double> errores)
		{
			this.errores = errores;
		}
		
		public List<Double> getErrores()
		{
			return errores;
		}
	}	
}
